/*******************************************************************************
 * Copyright (c) 2012-2017 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.inject;

import com.google.common.base.Splitter;
import com.google.common.io.Files;
import com.google.inject.AbstractModule;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.name.Names;
import com.google.inject.servlet.ServletModule;
import com.google.inject.util.Modules;
import com.google.inject.util.Providers;

import org.eclipse.che.inject.lifecycle.DestroyErrorHandler;
import org.eclipse.che.inject.lifecycle.DestroyModule;
import org.eclipse.che.inject.lifecycle.Destroyer;
import org.eclipse.che.inject.lifecycle.InitModule;
import org.everrest.guice.servlet.EverrestGuiceContextListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;

/**
 * CheBootstrap is entry point of Che application implemented as ServletContextListener.
 * <ul>
 * <li>Initializes Guice Injector</li>
 * <li>Automatically binds all the subclasses of com.google.inject.Module annotated with &#064DynaModule</li>
 * <li>Loads configuration from .properties and .xml files located in <i>/WEB-INF/classes/che</i> directory</li>
 * <li>Overrides it with external configuration located in directory pointed by <i>CHE_LOCAL_CONF_DIR</i> env variable (if any)</li>
 * <li>Binds all environment variables (visible as prefixed with "env.") and system properties (visible as prefixed with "sys.")</li>
 * <li>Thanks to Everrest integration injects all the properly annotated (see Everrest docs) REST Resources. Providers and ExceptionMappers
 * and inject necessary dependencies</li>
 * </ul>
 * <p/>
 * Configuration properties are bound as a {@code &#064Named}. For example:
 * Following entry in the .property file:
 * {@code myProp=value}
 * may be injected into constructor (other options are valid too of course) as following:
 * <pre>
 * &#064Inject
 * public MyClass(&#064Named("myProp") String my) {
 * }
 * </pre>
 * <p/>
 * It's possible to use system properties or environment variables in .properties files.
 * <pre>
 * my_app.input_dir=${root_data}/input/
 * my_app.output_dir=${root_data}/output/
 * </pre>
 * NOTE: System property always takes preference on environment variable with the same name.
 * <p/>
 * <table>
 * <tr><th>Value</th><th>System property</th><th>Environment variable</th><th>Result</th></tr>
 * <tr><td>${root_data}/input/</td><td>/home/andrew/temp</td><td>&nbsp;</td><td>/home/andrew/temp/input/</td></tr>
 * <tr><td>${root_data}/input/</td><td>&nbsp;</td><td>/usr/local</td><td>/usr/local/input/</td></tr>
 * <tr><td>${root_data}/input/</td><td>/home/andrew/temp</td><td>/usr/local</td><td>/home/andrew/temp/input/</td></tr>
 * <tr><td>${root_data}/input/</td><td>&nbsp;</td><td>&nbsp;</td><td>${root_data}/input/</td></tr>
 * </table>
 *
 * @author gazarenkov
 * @author andrew00x
 * @author Florent Benoit
 */
public class CheBootstrap extends EverrestGuiceContextListener {
    private static final Logger LOG = LoggerFactory.getLogger(CheBootstrap.class);

    /** Environment variable that is used to override some Che settings properties. */
    public static final String CHE_LOCAL_CONF_DIR = "CHE_LOCAL_CONF_DIR";

    public static final String PROPERTIES_ALIASES_CONFIG_FILE = "che_aliases.properties";

    /** Path to the internal folder that is expected in WEB-INF/classes */
    private static final String WEB_INF_RESOURCES = "che";

    /** Backward compliant path to the internal folder that is expected in WEB-INF/classes */
    private static final String COMPLIANT_WEB_INF_RESOURCES = "codenvy";

    private static final String NULL = "NULL";

    private final List<Module> modules = new ArrayList<>();

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        final ServletContext ctx = sce.getServletContext();
        final Injector injector = getInjector(ctx);
        if (injector != null) {
            injector.getInstance(Destroyer.class).destroy();
        }
        super.contextDestroyed(sce);
    }

    @Override
    protected List<Module> getModules() {
        // based on logic that getServletModule() is called BEFORE getModules() in the EverrestGuiceContextListener
        modules.add(new InitModule(PostConstruct.class));
        modules.add(new DestroyModule(PreDestroy.class, DestroyErrorHandler.DUMMY));
        modules.add(new URIConverter());
        modules.add(new URLConverter());
        modules.add(new FileConverter());
        modules.add(new PathConverter());
        modules.add(new StringArrayConverter());
        modules.add(new PairConverter());
        modules.add(new PairArrayConverter());
        modules.addAll(ModuleScanner.findModules());
        Map<String, Set<String>> aliases = readConfigurationAliases();
        Module firstConfigurationPermutation = Modules.override(new WebInfConfiguration(aliases)).with(new ExtConfiguration(aliases));
        Module secondConfigurationPermutation = Modules.override(firstConfigurationPermutation).with(new CheSystemPropertiesConfigurationModule(aliases));
        Module lastConfigurationPermutation = Modules.override(secondConfigurationPermutation).with(new CheEnvironmentVariablesConfigurationModule(aliases));
        modules.add(lastConfigurationPermutation);
        return modules;
    }

    private Map<String, Set<String>> readConfigurationAliases() {
        URL aliasesResource = getClass().getClassLoader().getResource(PROPERTIES_ALIASES_CONFIG_FILE);
        Map<String, Set<String>> aliases = new HashMap<>();
        if (aliasesResource != null) {
            Properties properties = new Properties();
            File aliasesFile = new File(aliasesResource.getFile());
            try (Reader reader = Files.newReader(aliasesFile, Charset.forName("UTF-8"))) {
                properties.load(reader);
            } catch (IOException e) {
                throw new IllegalStateException(String.format("Unable to read configuration aliases from file %s", aliasesFile), e);
            }
            for (Map.Entry<Object, Object> entry : properties.entrySet()) {
                String value = (String)entry.getValue();
                aliases.put((String)entry.getKey(),
                            Splitter.on(',').splitToList(value).stream().map(String::trim).collect(toSet()));
            }
        }
        return aliases;
    }

    /** see http://google-guice.googlecode.com/git/javadoc/com/google/inject/servlet/ServletModule.html */
    @Override
    protected ServletModule getServletModule() {
        // Servlets and other web components may be configured with custom Modules.
        return null;
    }

    /** ConfigurationModule binding configuration located in <i>/WEB-INF/classes/che</i> directory */
    static class WebInfConfiguration extends AbstractConfigurationModule {
        WebInfConfiguration(Map<String, Set<String>> aliases) {
            super(aliases);
        }

        protected void configure() {
            URL compliantWebInfConf = getClass().getClassLoader().getResource(COMPLIANT_WEB_INF_RESOURCES);
            if (compliantWebInfConf != null) {
                bindConf(new File(compliantWebInfConf.getFile()));
            }
            URL webInfConf = getClass().getClassLoader().getResource(WEB_INF_RESOURCES);
            if (webInfConf != null) {
                bindConf(new File(webInfConf.getFile()));
            }
        }
    }

    /**
     * ConfigurationModule binding environment variables, system properties and configuration in directory pointed by
     * <i>CHE_LOCAL_CONF_DIR</i> Env variable.
     */
    static class ExtConfiguration extends AbstractConfigurationModule {
        ExtConfiguration(Map<String, Set<String>> aliases) {
            super(aliases);
        }

        @Override
        protected void configure() {
            bindProperties("env.", System.getenv());
            bindProperties("sys.", System.getProperties());
            String extConfig = System.getenv(CHE_LOCAL_CONF_DIR);
            if (extConfig != null) {
                bindConf(new File(extConfig));
            }
        }
    }

    static class CheSystemPropertiesConfigurationModule extends AbstractConfigurationModule {
        CheSystemPropertiesConfigurationModule(Map<String, Set<String>> aliases) {
            super(aliases);
        }

        @Override
        protected void configure() {
            Iterable<Map.Entry<Object, Object>> cheProperties = System.getProperties().entrySet().stream()
                                                                      .filter(new PropertyNamePrefixPredicate<>("che.", "codenvy."))
                                                                      .collect(toList());
            bindProperties(null, cheProperties);
        }
    }

    static class CheEnvironmentVariablesConfigurationModule extends AbstractConfigurationModule {
        CheEnvironmentVariablesConfigurationModule(Map<String, Set<String>> aliases) {
            super(aliases);
        }

        @Override
        protected void configure() {
            Iterable<Map.Entry<String, String>> cheProperties = System.getenv().entrySet().stream()
                                                                      .filter(new PropertyNamePrefixPredicate<>("CHE_", "CODENVY_"))
                                                                      .map(new EnvironmentVariableToSystemPropertyFormatNameConverter())
                                                                      .collect(toList());
            bindProperties(null, cheProperties);
        }
    }

    static class PropertyNamePrefixPredicate<K, V> implements Predicate<Map.Entry<K, V>> {
        final String[] prefixes;

        PropertyNamePrefixPredicate(String... prefix) {
            this.prefixes = prefix;
        }

        @Override
        public boolean test(Map.Entry<K, V> entry) {
            for (String prefix : prefixes) {
                if (((String)entry.getKey()).startsWith(prefix)) {
                    return true;
                }
            }
            return false;
        }
    }

    static class PropertyNamePrefixRemover<K, V> implements Function<Map.Entry<K, V>, Map.Entry<String, V>> {
        final int prefixLength;

        PropertyNamePrefixRemover(int prefixLength) {
            this.prefixLength = prefixLength;
        }

        @Override
        public Map.Entry<String, V> apply(Map.Entry<K, V> entry) {
            return new SimpleEntry<>(((String)entry.getKey()).substring(prefixLength), entry.getValue());
        }
    }

    static class EnvironmentVariableToSystemPropertyFormatNameConverter implements Function<Map.Entry<String, String>, Map.Entry<String, String>> {
        @Override
        public Map.Entry<String, String> apply(Map.Entry<String, String> entry) {
            String name = entry.getKey();
            name = name.toLowerCase();
            // replace single underscore with dot and double underscores with single underscore
            // at first replace double underscores with equal sign which is forbidden in env variable name
            // then replace single underscores
            // then recover underscore from equal sign
            name = name.replace("__", "=");
            name = name.replace('_', '.');
            name = name.replace("=", "_");
            return new SimpleEntry<>(name, entry.getValue());
        }
    }

    private static final Pattern PROPERTIES_PLACE_HOLDER_PATTERN = Pattern.compile("\\$\\{[^\\}^\\$\\{]+\\}");

    static abstract class AbstractConfigurationModule extends AbstractModule {
        final Map<String, Set<String>> aliases;

        AbstractConfigurationModule(Map<String, Set<String>> aliases) {
            this.aliases = aliases;
        }

        protected void bindConf(File confDir) {
            final File[] files = confDir.listFiles();
            if (files != null) {
                for (File file : files) {
                    if (!file.isDirectory()) {
                        if ("properties".equals(Files.getFileExtension(file.getName()))) {
                            Properties properties = new Properties();
                            try (Reader reader = Files.newReader(file, Charset.forName("UTF-8"))) {
                                properties.load(reader);
                            } catch (IOException e) {
                                throw new IllegalStateException(String.format("Unable to read configuration file %s", file), e);
                            }
                            bindProperties(properties);
                        }
                    }
                }
            }
        }

        protected void bindProperties(Properties properties) {
            bindProperties(null, properties.entrySet());
        }

        protected void bindProperties(String prefix, Properties properties) {
            bindProperties(prefix, properties.entrySet());
        }

        protected void bindProperties(String prefix, Map<String, String> properties) {
            bindProperties(prefix, properties.entrySet(), true);
        }

        protected <K, V> void bindProperties(String prefix, Iterable<Map.Entry<K, V>> properties) {
            bindProperties(prefix, properties, false);
        }

        protected <K, V> void bindProperties(String prefix, Iterable<Map.Entry<K, V>> properties, boolean skipUnresolved) {
            StringBuilder buf = null;
            for (Map.Entry<K, V> e : properties) {
                String name = (String)e.getKey();
                String value = (String)e.getValue();
                if (NULL.equals(value)) {
                    bindProperty(prefix, name, null);
                } else {
                    final Matcher matcher = PROPERTIES_PLACE_HOLDER_PATTERN.matcher(value);
                    if (matcher.find()) {
                        int start = 0;
                        if (buf == null) {
                            buf = new StringBuilder();
                        } else {
                            buf.setLength(0);
                        }
                        do {
                            buf.append(value.substring(start, matcher.start()));
                            final String placeholder = value.substring(matcher.start(), matcher.end());
                            final String placeholderName = removePlaceholderFormatting(placeholder);
                            String resolvedPlaceholder = resolvePlaceholder(placeholderName);
                            if (resolvedPlaceholder != null) {
                                buf.append(resolvedPlaceholder);
                            } else if (skipUnresolved) {
                                buf.append(placeholder);
                                LOG.warn("Placeholder {} cannot be resolved neither from environment variable nor from system property, leaving as is.", placeholderName);
                            } else {
                                throw new ConfigurationException(
                                        String.format("Property %s is not found as system property or environment variable.", placeholderName));
                            }

                            start = matcher.end();
                        } while (matcher.find());
                        buf.append(value.substring(start));
                        value = buf.toString();
                    }
                    bindProperty(prefix, name, value);
                }
            }
        }

        private void bindProperty(String prefix, String name, String value) {
            String key = prefix == null ? name : (prefix + name);
            Set<String> aliasesForName = aliases.get(name);
            if (value == null) {
                bind(String.class).annotatedWith(Names.named(key)).toProvider(Providers.<String>of(null));
                if (aliasesForName != null) {
                    for (String alias : aliasesForName) {
                        bind(String.class).annotatedWith(Names.named(prefix == null ? alias : prefix + alias)).toProvider(Providers.<String>of(null));
                    }
                }
            } else {
                bindConstant().annotatedWith(Names.named(key)).to(value);
                if (aliasesForName != null) {
                    for (String alias : aliasesForName) {
                        bindConstant().annotatedWith(Names.named(prefix == null ? alias : prefix + alias)).to(value);
                    }
                }
            }
        }

        private String removePlaceholderFormatting(String placeholder) {
            return placeholder.substring(2, placeholder.length() - 1);
        }

        private String resolvePlaceholder(String placeholderName) {
            String resolved = System.getProperty(placeholderName);
            if (resolved == null) {
                resolved = System.getenv(placeholderName);
            }
            return resolved;
        }
    }
}
